﻿using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Cloo;
using RayDen.Library.Components.SystemComponents;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;
using RayDen.RayEngine.Core.Devices;
using RayDen.RayEngine.Core.Types;

namespace RayDen.RayEngine.IntersectionEngines.OpenCL {

    public class OpenClIntersectionDevice : IntersectionEngine {


        protected ClDeviceContext clContext;
        protected Stopwatch wallclock;
        protected double statsDeviceIdleTime;
        protected double statsDeviceTotalTime;
        protected Task renderingTask;
        protected CancellationTokenSource cancelToken;
        protected ConcurrentQueue<RayBuffer> todoRayBuffers, doneRayBuffers;
        //Buffers

        protected ComputeBuffer<Point> verts;
        protected ComputeBuffer<TriangleInfo> tris;
        protected ComputeBuffer<GpuSmitsBVHNode> tree;
        protected ComputeBuffer<RayData> rays;
        protected ComputeBuffer<RayHit> rayHits;


        public OpenClIntersectionDevice(RayEngineScene scene, bool lowLatency, int index)
            : base(scene) {
            wallclock = new Stopwatch();
            this.todoRayBuffers = new ConcurrentQueue<RayBuffer>();
            this.doneRayBuffers = new ConcurrentQueue<RayBuffer>();
            this.started = false;
            var device = ComputePlatform.Platforms[0].Devices[0];
            clContext = new ClDeviceContext() { KernelSrc = Kernels.LuxBVHKernel };
            clContext.Initialize();
            clContext.SetupDevice("Intersect");

            var rayBufferSize = lowLatency ? (RayBuffer.RayBufferSize / 8) : RayBuffer.RayBufferSize;
            var sceneVertices = scene.Vertices.ToArray();
            var sceneTriangles = scene.Triangles.Select(item=>item.GetInfo()).ToArray();
            Tracer.TraceLine("Vertices Data Size {0:F3} MBytes", (sceneVertices.Length * 12f) / (1024f * 1024f));
            Tracer.TraceLine("Indexes Data Size {0:F3} MBytes", (sceneTriangles.Length * 12f) / (1024f * 1024f));

            verts = new ComputeBuffer<Point>(clContext.context, ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer, sceneVertices);
            tris = new ComputeBuffer<TriangleInfo>(clContext.context, ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer, sceneTriangles);

            var st = DateTime.UtcNow;

            var da = new BvhDataAdapter(scene);
            var treeData = da.BuildData();
            
            Tracer.TraceLine("BVH Data Size {0:F3} MBytes" ,(treeData.Length*32f) / (1024f*1024f));
            tree = new ComputeBuffer<GpuSmitsBVHNode>(clContext.context, ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer, treeData);
            rays = new ComputeBuffer<RayData>(clContext.context, ComputeMemoryFlags.ReadOnly, rayBufferSize);
            rayHits = new ComputeBuffer<RayHit>(clContext.context, ComputeMemoryFlags.WriteOnly, rayBufferSize);
            Tracer.TraceLine("Bvh Build and Load Time {0}", DateTime.UtcNow - st);

            clContext.kernel.SetMemoryArgument(0, rays);
            clContext.kernel.SetMemoryArgument(1, rayHits);
            clContext.kernel.SetMemoryArgument(2, verts);
            clContext.kernel.SetMemoryArgument(3, tris);
            clContext.kernel.SetValueArgument(4, (uint)tris.Count);
            clContext.kernel.SetValueArgument(5, (uint)tree.Count);
            clContext.kernel.SetMemoryArgument(6, tree);
        }

        #region IIntersectionEngine Members

        public override void Stop() {
            wallclock.Stop();
            started = false;

            if (renderingTask.Status == TaskStatus.Running) {
                cancelToken.Cancel();
                renderingTask.Dispose();
                renderingTask = null;
            }

            this.todoRayBuffers = new ConcurrentQueue<RayBuffer>();
            this.doneRayBuffers = new ConcurrentQueue<RayBuffer>();
        }

        public void Interrupt() {
            wallclock.Stop();
            if (this.renderingTask.Status == TaskStatus.Running) {
                cancelToken.Cancel();
            }
        }

        public override void Start() {
            if (started)
                return;
            started = true;
            wallclock.Start();
            statsDeviceIdleTime = 0.0;
            statsDeviceTotalTime = 0.0;
            cancelToken = new CancellationTokenSource();
            this.renderingTask = Task.Factory.StartNew(() => RayIntersection(cancelToken.Token, this), cancelToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current);
        }

        public override void Trace(RayBuffer rayBuffer) {
            var device = this;
            var raysArg = rayBuffer.RaysInfo;
            GCHandle raysHandle = GCHandle.Alloc(raysArg, GCHandleType.Pinned);
            clContext.kernel.SetValueArgument(7, rayBuffer.GetRayCount());
            device.clContext.commands.Write(device.rays, true, 0, rayBuffer.Size, raysHandle.AddrOfPinnedObject(), null);
            device.clContext.commands.Execute(device.clContext.kernel, null, new long[] { rayBuffer.RaysInfo.Length }, new long[] { 32 }, null);
            //device.clContext.commands.Finish();
            GCHandle arrCHandle = GCHandle.Alloc(rayBuffer.rayHits, GCHandleType.Pinned);
            device.clContext.commands.Read(device.rayHits, true, 0, rayBuffer.Size, arrCHandle.AddrOfPinnedObject(), null);
            //device.clContext.commands.Finish();
            rayBuffer.traced = true;
            arrCHandle.Free();
            raysHandle.Free();
        }

        protected static void RayIntersection(CancellationToken ct, OpenClIntersectionDevice device) {
#if VERBOSE
            try
            {
                
#endif            
            Tracer.TraceLine("Ray-triangle intersection device {0}", device.GetType().Name);
            while (!ct.IsCancellationRequested) {
                //var t1 = 0;// device.wallclock.ElapsedTicks;
                RayBuffer rayBuffer;

#if OLD 
                if (!device.todoRayBuffers.TryDequeue(out rayBuffer))
                    continue;

#else
                if (!device.todoRayBuffers.TryDequeue(out rayBuffer)) {
                    continue;
                }
#endif

                //var t2 = DateTime.UtcNow.Ticks;
                device.Trace(rayBuffer);
                //Tracer.TraceLine("Result returns {0} items ", buffer.rayHits.Length);
                //            Tracer.TraceLine("Intersections {0} items ", buffer.rayHits.Count(p => p.index != uint.MaxValue));
                //var t3 = device.wallclock.ElapsedTicks;

                //device.statsDeviceIdleTime += t2 - t1;
                //device.statsDeviceTotalTime += t3 - t1;
                device.statsTotalRayCount += rayBuffer.RaysInfo.Length;
                device.doneRayBuffers.Enqueue(rayBuffer);
            }

#if VERBOSE
            }
            catch (Exception ex)
            {
                Tracer.TraceLine("Intersection device exception");
                //Tracer.TraceLine("Error triangle {0}", currentTriangleIndex);
                Tracer.TraceLine(ex.Message);
                Tracer.TraceLine(ex.StackTrace);

            }
#endif
        }


        public override void PushRayBuffer(RayBuffer rayBuffer,  int clientIdx) {
            todoRayBuffers.Enqueue(rayBuffer);
        }

        public override RayBuffer PopRayBuffer( int info) {
            RayBuffer result;
            doneRayBuffers.TryDequeue(out result);
            return result;
        }

        #endregion

        #region IDisposable Members

        public override void Dispose() {

            if (clContext.context != null) {
                
                foreach (var buffer in new ComputeMemory[] { rayHits, rays, tree, tris, verts }) {
                    buffer.Dispose();
                }
                clContext.commands.Dispose();
                clContext.program.Dispose();
                clContext.kernel.Dispose();
                clContext.context.Dispose();
            }

        }

        #endregion
    }
}
